/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package OLDSTUFF.HierarchyPackage.ProductHierarchy;

import OLDSTUFF.HierarchyPackage.*;
import OLDSTUFF.HierarchyPackage.AffixHierarchies.SuffixHierarchy;
import OLDSTUFF.ClassifiedDataSetPackage.ClassifiedDataSet;
import OLDSTUFF.ClassifiedDataSetPackage.ClassifiedDataSetStorage;
import OLDSTUFF.DataSetPackage.DataSetException;
import OLDSTUFF.DataSetPackage.ProductView;
import OLDSTUFF.DataSetPackage.View;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.osdtsystem.utils.MemoryStorage;

/**
 *
 * @author Martin Haulrich
 */
public class ProductHierarchy implements Hierarchy {

    /**
     * Hierarchies that this hierarchy is an intersection of.
     */
    Hierarchy[] hierarchies;

    /**
     * The TOP-class is this hierarchy.
     */
    ProductHClass top;

    public ProductHierarchy() {
    }

    /**
     * Constructor.
     * Construct a new intersection hierarchy from existing hierarchies.
     * @param hierarchies Hierarchies that this hierarchy should be an intersection of.
     * @throws HierarchyPackage.HierarchyException Exception thrown when there are problems accesing the hierarchy.
     */
    public ProductHierarchy(Hierarchy[] hierarchies) throws HierarchyException {
        this.hierarchies = hierarchies;
        HClass[] topClasses = new HClass[hierarchies.length];
        for (int i = 0; i < hierarchies.length; i++) {
            topClasses[i] = hierarchies[i].getTop();
        }
        top = new ProductHClass(this, topClasses);
    }

    public Hierarchy[] getHierarchies() {
        return hierarchies;
    }

    @Override
    public ClassifiedDataSet getClassifications(Collection<Integer> dids, View dv, HClass topClass,
            int maxDepth, int minCount, int cutFraction, boolean saveData) throws HierarchyException, DataSetException {

        ClassifiedDataSetStorage result = new ClassifiedDataSetStorage(new MemoryStorage(), minCount, cutFraction);

        ProductView pv = (ProductView) dv;
        ClassifiedDataSet[] dsets = new ClassifiedDataSetStorage[hierarchies.length];

        //HClass[] topClasses = top.getID();
        HClass[] topClasses = ((ProductHClass) topClass).getID();

        View[] views = pv.getViews();
        System.err.println("Classifying dimensions...");
        // Classify in all the hierarchies individually
        for (int i = 0; i < hierarchies.length; i++) {
            dsets[i] = hierarchies[i].getClassifications(dids, views[i], topClasses[i], maxDepth, minCount, cutFraction, false);
        }


        System.err.println("Classifying products...");


        result.addClassificationWithData(topClass, 0, dids);

        int[] currentDepthDim = new int[topClasses.length];
        Arrays.fill(currentDepthDim, 0);
        int remainingDepth = maxDepth;
        HClass[] newClasses = new HClass[topClasses.length];
        System.arraycopy(topClasses, 0, newClasses, 0, topClasses.length);
        for (int i = 0; i < topClasses.length; i++) {
            currentDepthDim[i]++;
            for (HClass c : dsets[i].classificationsAtDepth(currentDepthDim[i])) {
                if (hierarchies[i].superclassOf(topClasses[i], c)) {
                    newClasses[i] = c;
                    recClassify(newClasses, remainingDepth, maxDepth, currentDepthDim, dids, pv, dsets, minCount, result);
                }
            }
            currentDepthDim[i]--;
            newClasses[i] = topClasses[i];
        }

        return result;
    }

    private void recClassify(HClass[] classes, int remainingDepth, int maxDepth, int[] currentDepthDim,
            Collection<Integer> data, ProductView pv, ClassifiedDataSet[] dsets,
            int minCount, ClassifiedDataSet result) throws HierarchyException, DataSetException {

        // System.err.println(Arrays.toString(currentDepthDim));
        ProductHClass pc = new ProductHClass(this, classes);
        //  System.err.println(pc.getName());
        if (result.containsClass(pc)) {
            return;
        }
        List<Integer> myData = new ArrayList<Integer>();
        for (Integer d : data) {
            if (isClassifiedBy(d, pv, pc)) {
                myData.add(d);
            }
        }

        //if (myData.isEmpty()) {
        if (myData.size() < minCount) {
            return;
        }
        result.addClassificationWithData(pc, maxDepth - remainingDepth, myData);

        if (remainingDepth != 0) {
            HClass[] newClasses = new HClass[classes.length];
            System.arraycopy(classes, 0, newClasses, 0, classes.length);
            for (int i = 0; i < classes.length; i++) {
                if (currentDepthDim[i] < maxDepth) {
                    currentDepthDim[i]++;
                    for (HClass c : dsets[i].classificationsAtDepth(currentDepthDim[i])) {
                        if (hierarchies[i].superclassOf(classes[i], c)) {
                            newClasses[i] = c;
                            recClassify(newClasses, remainingDepth - 1, maxDepth, currentDepthDim, myData, pv, dsets, minCount, result);
                        }
                    }
                    currentDepthDim[i]--;
                }
                newClasses[i] = classes[i];
            }
        }
    }

    @Override
    public boolean isClassifiedBy(Integer did, View dv, HClass classification) throws HierarchyException, DataSetException {

        ProductView pv = (ProductView) dv;
        ProductHClass phc = (ProductHClass) classification;



        //     System.err.println(Arrays.toString(dids));
        View[] views = pv.getViews();
        HClass[] classes = phc.getID();

        // All points should be classified in their respective hierarchies in order for the product-point to be classified.
        boolean classified = true;
        for (int i = 0; i < views.length && classified; i++) {
            classified = classified && hierarchies[i].isClassifiedBy(did, views[i], classes[i]);
        }
        return classified;
    }

    @Override
    public Set<HClass> getIntersection(HClass c1, HClass c2) throws HierarchyException {

        Set<HClass> result = new HashSet<HClass>();

        if (superclassOf(c1, c2)) {
            result.add(c2);
            return result;
        }

        if (superclassOf(c2, c1)) {
            result.add(c1);
            return result;
        }

        HClass[] pc1 = ((ProductHClass) c1).getID();
        HClass[] pc2 = ((ProductHClass) c2).getID();

        HClass[] newClass = new HClass[pc1.length];
        boolean reduced = false;

        for (int k = 0; k < pc1.length; k++) {
            HClass iClass = pc1[k];
            HClass jClass = pc2[k];
//            System.err.println(iClass.getName() + " XX " + jClass.getName());
//            System.err.println(hierarchies[k].getIntersection(iClass, jClass));
            if (hierarchies[k].getIntersection(iClass, jClass).isEmpty()) {

                return new HashSet<HClass>();
            }
            // ex: (TOP x der) intersect (ar X er) = (ar x der)
            newClass[k] = iClass;
            if (hierarchies[k].superclassOf(iClass, jClass)) {
                newClass[k] = jClass;
                reduced = true;
            }
            if (hierarchies[k].superclassOf(jClass, iClass)) {
                newClass[k] = iClass;
                reduced = true;
            }
        }

        if (reduced) {
            result.add(new ProductHClass(this, newClass));
            return result;
        }


        // Nothing to do. Just return input
        result.add(c1);
        result.add(c2);
        //  System.err.println("PH, getInter.pair  IN: " + c1 + "," + c2 + "   OUT: " + result);
        return result;
    }

    @Override
    public Set<HClass> getIntersection(HClass[] classes) throws HierarchyException {


        Set<HClass> result = new HashSet<HClass>();


        for (HClass c : classes) {
            if (!(c instanceof ProductHClass)) {
                System.err.println("At least one class is not a ProductHClass");
                return result;
            }
        }

        // In order not to change the datastructure while iterating over it, we save classes that
        // are to be deleted in a set
        Set<HClass> removeSet = new HashSet<HClass>();
        // And the same for classes that are to be added, which is the case when the intersection 
        // of two classes can be expressed as one class. Then the two should be removed and the one added.
        Set<HClass> addSet = new HashSet<HClass>();

        for (int i = 0; i < classes.length; i++) {
            ProductHClass ci = (ProductHClass) classes[i];
            for (int j = i + 1; j < classes.length; j++) {
                ProductHClass cj = (ProductHClass) classes[j];
                if (superclassOf(ci, cj)) {
                    removeSet.add(ci);
                } else {
                    if (superclassOf(cj, ci)) {
                        removeSet.add(cj);
                    }
                }

                HClass[] iClasses = ci.getID();
                HClass[] jClasses = cj.getID();
                boolean changed = false;
                HClass[] newClasses = new HClass[iClasses.length];

                // Run through all the classes of the two current product-classes
                for (int k = 0; k < iClasses.length; k++) {
                    HClass iClass = iClasses[k];
                    HClass jClass = jClasses[k];
                    HClass[] ij = {iClass, jClass};
                    // If corresponding classes in productclass are disjunctive the intersection of the productclasses is empty.
                    if (iClass.belongsTo().getIntersection(ij).isEmpty()) {
                        return new HashSet<HClass>();
                    }
                    // ex: (TOP x der) intersect (ar X er) = (ar x der)
                    // As default the class of a eventual new ProductClass is unchanged
                    newClasses[k] = iClass;
                    if (iClass.belongsTo().superclassOf(iClass, jClass)) {
                        newClasses[k] = jClass;
                        changed = true;
                    }
                    if (iClass.belongsTo().superclassOf(jClass, iClass)) {
                        newClasses[k] = iClass;
                        changed = true;
                    }
                }
                if (changed) {
                    ProductHClass newClass = new ProductHClass(this, newClasses);
                    removeSet.add(cj);
                    removeSet.add(ci);
                    addSet.add(newClass);
                }
            }

        }
        result = new HashSet<HClass>(Arrays.asList(classes));
        result.removeAll(removeSet);
        result.addAll(addSet);

        System.err.print("-------------- GETINTERSECTION: ");
        for (HClass c : classes) {
            System.err.print(c.getName() + " ");
        }
        System.err.print(" -> ");
        for (HClass c : result) {
            System.err.print(c.getName() + " ");
        }
        System.err.println("");


        return result;
    }

    @Override
    public String getName(HClass c) throws HierarchyException {
        ProductHClass ic = (ProductHClass) c;

        HClass[] cc = ic.getID();

        String name = "";
        boolean first = true;
        for (int i = 0; i < hierarchies.length; i++) {

            if (first) {
                name = hierarchies[i].getName(cc[i]);
                first = false;
            } else {
                /**
                 * @TODO Find an appropriate unicode product symbol
                 */
                // Unicode causes problems in PS so for now we use x
                name = name + " x " + hierarchies[i].getName(cc[i]);
            //name = name + " \u2715 " + hierarchies[i].getName(cc[i]);
            }
        }
        return name;
    }

    @Override
    public boolean superclassOf(HClass c1, HClass c2) throws HierarchyException {


        if (!((c1 instanceof ProductHClass) && (c2 instanceof ProductHClass))) {
            return false;
        }

        ProductHClass pc1 = (ProductHClass) c1;
        ProductHClass pc2 = (ProductHClass) c2;

        // are all classes from c1 superclasses or equal to the corresponding classes in c2
        boolean allsuporeq = true;
        // is at lest one class from c1 a superclass to the corresponding class in c2
        boolean existssuper = false;
        HClass[] classes1 = pc1.getID();
        HClass[] classes2 = pc2.getID();
        if (classes1.length != classes2.length) {
            return false;
        }
        for (int i = 0; i < classes1.length && allsuporeq; i++) {
            boolean localSuper = hierarchies[i].superclassOf(classes1[i], classes2[i]);
            allsuporeq = allsuporeq && (localSuper || (classes1[i].equals(classes2[i])));
            if (localSuper) {
                existssuper = true;
            }
        }
        return allsuporeq && existssuper;
    }

    @Override
    public HClass getTop() {
        return top;
    }

    @Override
    public Map<HClass, Integer> saveEst(String filename, Set<HClass> classes) throws IOException, HierarchyException {

        int id = 0;

        // For each index in product-class save the classes that are present at this index among the 'classes'
        // This means, find out which classes from each of the hiearchies is used in the ProductClasses
        List<Set<HClass>> myClasses = new ArrayList<Set<HClass>>(hierarchies.length);
        for (int i = 0; i < hierarchies.length; i++) {
            myClasses.add(i, new HashSet<HClass>());
        }
        for (HClass c : classes) {

            //     System.err.println(c.getName());
            ProductHClass pc = (ProductHClass) c;
            HClass[] pcc = pc.getID();

            for (int i = 0; i < pcc.length; i++) {
                myClasses.get(i).add(pcc[i]);
            }

        }

        //   System.err.println(myClasses.get(0));

        // Create map from class to ID for each of the hierarchies in the product-hierarchy
        List<Map<HClass, Integer>> classMaps = new ArrayList<Map<HClass, Integer>>(hierarchies.length);
        for (int i = 0; i < hierarchies.length; i++) {
            classMaps.add(hierarchies[i].saveEst(filename + "-" + i, myClasses.get(i)));
        }

        PrintWriter out = new PrintWriter(new FileWriter(filename));
        out.println("ProductHierarchy");
        StringBuffer strBuf = new StringBuffer();
        for (int i = 0; i < hierarchies.length; i++) {
            strBuf.append(hierarchies[i].getType() + ",");
        }
        strBuf.deleteCharAt(strBuf.length() - 1);
        strBuf.append(System.getProperty("line.separator"));

        for (int i = 0; i < hierarchies.length; i++) {
            strBuf.append(filename + "-" + i + ",");
        }
        strBuf.deleteCharAt(strBuf.length() - 1);
        out.println(strBuf);

        // Map to return.
        Map<HClass, Integer> returnMap = new HashMap<HClass, Integer>();

        for (HClass c : classes) {
            strBuf = new StringBuffer();
            strBuf.append(id + "\t");
            returnMap.put(c, id);
            id++;
            ProductHClass pc = (ProductHClass) c;
            HClass[] pcc = pc.getID();
            for (int i = 0; i < pcc.length; i++) {
                long cID = classMaps.get(i).get(pcc[i]);
                strBuf.append(cID + ",");
            }
            strBuf.deleteCharAt(strBuf.length() - 1);
            out.println(strBuf);

        }
        out.close();
        return returnMap;
    }

    @Override
    public Map<Integer, HClass> loadEst(String filename, Set<Integer> classIDs) throws IOException, HierarchyException {


        BufferedReader input = new BufferedReader(new FileReader(filename));
        String line;
        line = input.readLine();
        if (!line.equals("ProductHierarchy")) {
            System.err.println("ProductHierarchy: ERROR in first line");
        }

        // Get types for saved hierarchies
        line = input.readLine();
        String[] types = line.split(",");

        // Get filenames for saved id->class maps.
        line = input.readLine();
        String[] fileNames = line.split(",");
        int productSize = fileNames.length;

        // Make lists off all ids from the different hierarchies
        List<Set<Integer>> hierIDs = new ArrayList<Set<Integer>>(productSize);

        // Add empty sets to each index in list
        for (int i = 0; i < productSize; i++) {
            hierIDs.add(new HashSet<Integer>());
        }

        // To avoid running through the file again
        Map<Integer, int[]> ids = new HashMap<Integer, int[]>();

        // Find the IDs
        while ((line = input.readLine()) != null) {
            String[] elements = line.split("\t");
            String[] classes = elements[1].split(",");
            int[] idClasses = new int[productSize];
            for (int i = 0; i < classes.length; i++) {
                hierIDs.get(i).add(Integer.valueOf(classes[i]));
                idClasses[i] = Integer.valueOf(classes[i]);
            }
            Integer id = Integer.valueOf(elements[0]);
            ids.put(id, idClasses);
        }

        // Now load the hierarchies to get the ID-to-HClass maps
        List<Map<Integer, HClass>> hierMaps = new ArrayList<Map<Integer, HClass>>(productSize);

        /**
         * XXX FIX THIS: For now we just call the SuffixHierarchy because we use this for testing.
         * Either the type of hierachies should be given by the ProductHier file and this info used to call the right hierarhcy
         * Alternatively make a commen loadEst method.  
         */
        hierarchies = new Hierarchy[productSize];
        for (int i = 0; i < productSize; i++) {
            if (types[i].equals("SuffixHierarchy")) {
                hierarchies[i] = new SuffixHierarchy();
            } else {
                throw new UnsupportedOperationException("NOT IMPLEMENTED YET");
            }
//            System.err.println(fileNames[i]);
            hierMaps.add(i, hierarchies[i].loadEst(fileNames[i], hierIDs.get(i)));

        }
        int j = 0;
//        for (Map<Long, HClass> m : hierMaps) {
//            for (Entry<Long, HClass> e : m.entrySet()) {
//                System.err.println(j + "\t" + e.getKey() + " ; " + e.getValue().getName());
//            }
//            j++;
//        }

        Map<Integer, HClass> result = new HashMap<Integer, HClass>();
        for (Integer id : classIDs) {
            HClass[] classes = new HClass[productSize];
            for (int i = 0; i < productSize; i++) {
                Integer cid = ids.get(id)[i];
                HClass c = hierMaps.get(i).get(cid);
                classes[i] = c;

            }
            ProductHClass pc = new ProductHClass(this, classes);
            result.put(id, pc);
        }

        return result;
    }

    @Override
    public HClass newClass(Integer did, View v) throws DataSetException {
        ProductView pv = (ProductView) v;
        View[] views = pv.getViews();
        HClass[] classes = new HClass[views.length];
        for (int i = 0; i < views.length; i++) {
            classes[i] = hierarchies[i].newClass(did, pv.getViews()[i]);
        }
        return new ProductHClass(this, classes);

    }

    @Override
    public String getType() {
        return "ProductHierarchy";
    }
}
